package com.enci.usbserial;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Base64;
import android.util.Log;

import com.usbserial.core.DataListener;
import com.usbserial.core.SerialConfig;
import com.usbserial.core.SerialPort;
import com.usbserial.core.UsbPermissionCallback;
import com.usbserial.core.UsbSerialDeviceInfo;
import com.usbserial.core.UsbSerialManager;
import com.usbserial.exception.PermissionDeniedException;
import com.usbserial.exception.UsbSerialException;

import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;

import org.json.JSONObject;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;

import io.dcloud.feature.uniapp.annotation.UniJSMethod;
import io.dcloud.feature.uniapp.bridge.UniJSCallback;
import io.dcloud.feature.uniapp.common.UniModule;

/**
 * USB Serial UniApp 插件模块
 * 
 * 基于 usbserial-master 库实现的 USB 串口通信 UniApp 桥接模块。
 * 提供以下核心功能：
 * 
 * 1. 设备管理：
 *    - listDevices(): 列出所有可用的 USB 串口设备
 *    - requestPermission(): 请求 USB 设备访问权限
 * 
 * 2. 连接管理：
 *    - open(): 打开串口连接，创建会话
 *    - close(): 关闭串口连接，清理会话
 *    - getStatus(): 获取会话状态信息
 * 
 * 3. 数据通信：
 *    - write(): 向串口写入数据（支持文本、十六进制、Base64）
 *    - subscribe(): 订阅串口数据接收事件
 *    - unsubscribe(): 取消数据接收订阅
 * 
 * 4. 权限处理：
 *    - 集成 UsbPermissionReceiver 处理权限请求响应
 *    - 支持异步权限请求和回调处理
 * 
 * 5. 会话管理：
 *    - 支持多设备并发连接
 *    - 自动资源清理和异常处理
 *    - 线程安全的会话状态管理
 * 
 * 使用示例：
 * ```javascript
 * // 列出设备
 * const devices = await usbSerial.listDevices();
 * 
 * // 请求权限
 * await usbSerial.requestPermission({ deviceId: devices[0].deviceId });
 * 
 * // 打开连接
 * const session = await usbSerial.open({ 
 *   deviceId: devices[0].deviceId,
 *   config: { baudRate: 9600, dataBits: 8, stopBits: 1, parity: 0 }
 * });
 * 
 * // 订阅数据
 * usbSerial.subscribe({ sessionId: session.sessionId }, (data) => {
 *   console.log('Received:', data.payload);
 * });
 * 
 * // 发送数据
 * await usbSerial.write({ sessionId: session.sessionId, data: 'Hello' });
 * ```
 * 
 * @author USB Serial Plugin Team
 * @version 1.0.0
 * @since 2024
 */
public class UsbSerialModule extends UniModule {

    /**
     * 测试方法 - 验证参数传递是否正常
     */
    @UniJSMethod(uiThread = false)
    public void testEcho(JSONObject options, UniJSCallback callback) {
        Log.d(LOG_TAG, "[DEBUG] testEcho() called");
        Log.d(LOG_TAG, "[DEBUG] testEcho() options type: " + (options != null ? options.getClass().getName() : "null"));
        Log.d(LOG_TAG, "[DEBUG] testEcho() options content: " + options);
        Log.d(LOG_TAG, "[DEBUG] testEcho() options.toString(): " + (options != null ? options.toString() : "null"));

        if (callback == null) {
            Log.w(LOG_TAG, "testEcho called without callback");
            return;
        }

        try {
            Map<String, Object> result = successResult();
            result.put("message", "testEcho success");
            result.put("receivedOptions", options != null ? options.toString() : "null");
            if (options != null) {
                result.put("optionsIsEmpty", options.length() == 0);
                result.put("optionsKeys", options.keys().toString());
            }
            callback.invoke(result);
        } catch (Exception e) {
            Log.e(LOG_TAG, "[ERROR] testEcho() exception", e);
            callback.invoke(errorResult("test_error", e.getMessage()));
        }
    }

    /**
     * Hello方法 - 用于测试插件是否正常加载
     * @return 插件基本信息
     */
    @UniJSMethod(uiThread = false)
    public void hello(JSONObject options, UniJSCallback callback) {
        Log.d(LOG_TAG, "[HELLO_JSON] hello(JSONObject, UniJSCallback) 方法被调用");
        Log.d(LOG_TAG, "[HELLO_JSON] options=" + options);
        
        if (callback == null) {
            Log.w(LOG_TAG, "[HELLO_JSON] called without callback");
            return;
        }
        
        try {
            Map<String, Object> result = successResult();
            result.put("message", "USB Serial Plugin 加载成功");
            result.put("version", "1.0.0");
            result.put("pluginId", "usb-serial-uniapp-plugin");
            result.put("timestamp", System.currentTimeMillis());
            
            // 添加设备管理器状态
            Context context = null;
            if (mWXSDKInstance != null) {
                context = mWXSDKInstance.getContext();
            }
            if (context == null && mUniSDKInstance != null) {
                context = mUniSDKInstance.getContext();
            }
            
            if (context != null) {
                UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
                if (usbManager != null) {
                    result.put("usbManagerAvailable", true);
                    result.put("deviceCount", usbManager.getDeviceList().size());
                } else {
                    result.put("usbManagerAvailable", false);
                }
            } else {
                result.put("contextAvailable", false);
            }
            
            Log.d(LOG_TAG, "[HELLO_JSON] 返回结果: " + result.toString());
            callback.invoke(result);
        } catch (Exception e) {
            Log.e(LOG_TAG, "[HELLO_JSON][ERROR] 方法执行异常: " + e.getMessage(), e);
            callback.invoke(errorResult("hello_error", "Hello(JSON)方法执行异常: " + e.getMessage()));
        }
    }

    /**
     * hello 方法的重载版本：用于测试桥接层的多参数传递
     */
    @UniJSMethod(uiThread = false)
    public void hello(String arg1, int arg2, JSONObject options, UniJSCallback callback) {
        Log.d(LOG_TAG, "[HELLO_ARGS] hello(String,int,JSONObject,UniJSCallback) 被调用");
        Log.d(LOG_TAG, "[HELLO_ARGS] arg1=" + arg1 + ", arg2=" + arg2);
        Log.d(LOG_TAG, "[HELLO_ARGS] options=" + options);
        
        if (callback == null) {
            Log.w(LOG_TAG, "[HELLO_ARGS] called without callback");
            return;
        }
        
        try {
            Map<String, Object> result = successResult();
            result.put("nativeInvoked", true);
            result.put("variant", "hello_args");
            result.put("signature", "hello(String,int,JSONObject,UniJSCallback)");
            result.put("thread", Thread.currentThread().getName());
            result.put("message", "USB Serial Plugin hello with params");
            result.put("version", "1.0.0");
            result.put("pluginId", "usb-serial-uniapp-plugin");
            result.put("timestamp", System.currentTimeMillis());
            
            result.put("arg1", arg1);
            result.put("arg2", arg2);
            result.put("receivedOptions", options != null ? options.toString() : "null");
            if (options != null) {
                result.put("optionsIsEmpty", options.length() == 0);
            }
            
            Log.d(LOG_TAG, "[HELLO_ARGS] 返回结果: " + result.toString());
            callback.invoke(result);
        } catch (Exception e) {
            Log.e(LOG_TAG, "[HELLO_ARGS][ERROR] 方法执行异常: " + e.getMessage(), e);
            callback.invoke(errorResult("hello_args_error", "Hello(带参数)方法执行异常: " + e.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void hello(UniJSCallback callback) {
        Log.d(LOG_TAG, "[HELLO_ZERO] hello(UniJSCallback) 0参方法被调用");
        
        if (callback == null) {
            Log.w(LOG_TAG, "[HELLO_ZERO] called without callback");
            return;
        }
        
        try {
            Map<String, Object> result = successResult();
            result.put("nativeInvoked", true);
            result.put("variant", "hello_zero");
            result.put("signature", "hello(UniJSCallback)");
            result.put("thread", Thread.currentThread().getName());
            result.put("message", "USB Serial Plugin hello zero-args");
            result.put("version", "1.0.0");
            result.put("pluginId", "usb-serial-uniapp-plugin");
            result.put("timestamp", System.currentTimeMillis());
            
            Log.d(LOG_TAG, "[HELLO_ZERO] 返回结果: " + result.toString());
            callback.invoke(result);
        } catch (Exception e) {
            Log.e(LOG_TAG, "[HELLO_ZERO][ERROR] 方法执行异常: " + e.getMessage(), e);
            callback.invoke(errorResult("hello_zero_error", "Hello(0参)方法执行异常: " + e.getMessage()));
        }
    }

    private static final String LOG_TAG = "UsbSerialModule";
    private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private final Handler mainHandler;
    private final ConcurrentMap<String, SerialSession> sessions = new ConcurrentHashMap<>();
    private volatile UsbSerialManager serialManager;
    private UsbPermissionReceiver permissionReceiver;
    private final Map<Integer, UniJSCallback> pendingPermissionCallbacks = new ConcurrentHashMap<>();

    public UsbSerialModule() {
        Handler handler;
        try {
            handler = new Handler(Looper.getMainLooper());
        } catch (RuntimeException error) {
            handler = null;
        }
        this.mainHandler = handler;
    }

    @Override
    public void onActivityDestroy() {
        super.onActivityDestroy();
        dispose();
    }

    private void dispose() {
        // 注销权限接收器
        if (permissionReceiver != null && mUniSDKInstance != null && mUniSDKInstance.getContext() != null) {
            try {
                mUniSDKInstance.getContext().unregisterReceiver(permissionReceiver);
            } catch (Exception e) {
                Log.w(LOG_TAG, "Error unregistering permission receiver", e);
            }
            permissionReceiver = null;
        }
        
        // 清理待处理的权限回调
        for (UniJSCallback callback : pendingPermissionCallbacks.values()) {
            if (callback != null) {
                callback.invoke(errorResult("DISPOSED", "Module disposed before permission result"));
            }
        }
        pendingPermissionCallbacks.clear();
        
        for (SerialSession session : sessions.values()) {
            session.closeSilently();
        }
        sessions.clear();
        UsbSerialManager manager = serialManager;
        serialManager = null;
        if (manager != null) {
            try {
                manager.close();
            } catch (IOException ignored) {
            }
        }
    }

    private void runOnMain(Runnable runnable) {
        Handler handler = mainHandler;
        if (handler != null) {
            handler.post(runnable);
        } else {
            runnable.run();
        }
    }

    private UsbSerialManager ensureSerialManager() {
        UsbSerialManager manager = serialManager;
        if (manager != null) {
            return manager;
        }
        Context context = null;
        if (mWXSDKInstance != null) {
            context = mWXSDKInstance.getContext();
        }
        if (context == null && mUniSDKInstance != null) {
            context = mUniSDKInstance.getContext();
        }
        if (context == null) {
            throw new IllegalStateException("Context unavailable for UsbSerialManager");
        }
        manager = new UsbSerialManager(context);
        serialManager = manager;
        return manager;
    }

    private void ensurePermissionReceiver() {
        if (permissionReceiver == null && mUniSDKInstance != null && mUniSDKInstance.getContext() != null) {
            synchronized (this) {
                if (permissionReceiver == null) {
                    permissionReceiver = new UsbPermissionReceiver(this::handlePermissionResult);
                    IntentFilter filter = new IntentFilter(UsbPermissionReceiver.ACTION_USB_PERMISSION);
                    mUniSDKInstance.getContext().registerReceiver(permissionReceiver, filter);
                }
            }
        }
    }

    private void handlePermissionResult(UsbDevice device, boolean granted) {
        if (device == null) {
            Log.w(LOG_TAG, "handlePermissionResult: device is null");
            return;
        }
        
        int deviceId = device.getDeviceId();
        UniJSCallback callback = pendingPermissionCallbacks.remove(deviceId);
        
        if (callback != null) {
            runOnMain(() -> {
                if (granted) {
                    Map<String, Object> result = successResult();
                    result.put("granted", true);
                    callback.invoke(result);
                } else {
                    callback.invoke(errorResult("permission_denied", "User denied USB permission"));
                }
            });
        } else {
            Log.w(LOG_TAG, "No pending callback found for device " + deviceId);
        }
    }

    @UniJSMethod(uiThread = false)
    public void listDevicesWithString(String optionsJson, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] listDevicesWithString invoked with optionsJson=" + optionsJson);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] listDevicesWithString called without callback");
            return;
        }
        try {
            JSONObject options = null;
            if (optionsJson != null && !optionsJson.isEmpty()) {
                options = new JSONObject(optionsJson);
            }
            Log.i(LOG_TAG, "[DEBUG] listDevicesWithString parsed options=" + options);

            UsbSerialManager manager = ensureSerialManager();
            List<UsbSerialDeviceInfo> devices = manager.listDevices();
            List<Map<String, Object>> payload = new ArrayList<>(devices.size());
            for (UsbSerialDeviceInfo info : devices) {
                payload.add(deviceInfoToMap(info, manager.hasPermission(info.getUsbDevice())));
            }
            Map<String, Object> result = successResult();
            result.put("devices", payload);
            callback.invoke(result);
            Log.i(LOG_TAG, "[DEBUG] listDevicesWithString returning " + payload.size() + " devices");
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] listDevicesWithString failed", ex);
            callback.invoke(errorResult("list_devices_error", ex.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void listDevices(JSONObject options, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] listDevices invoked with options=" + options);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] listDevices called without callback");
            return;
        }
        try {
            UsbSerialManager manager = ensureSerialManager();
            List<UsbSerialDeviceInfo> devices = manager.listDevices();
            List<Map<String, Object>> payload = new ArrayList<>(devices.size());
            for (UsbSerialDeviceInfo info : devices) {
                payload.add(deviceInfoToMap(info, manager.hasPermission(info.getUsbDevice())));
            }
            Map<String, Object> result = successResult();
            result.put("devices", payload);
            callback.invoke(result);
            Log.i(LOG_TAG, "[DEBUG] listDevices returning " + payload.size() + " devices");
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] listDevices failed", ex);
            callback.invoke(errorResult("list_devices_error", ex.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void requestPermissionWithString(String optionsJson, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] requestPermissionWithString invoked with optionsJson=" + optionsJson);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] requestPermissionWithString called without callback");
            return;
        }
        try {
            JSONObject options = null;
            if (optionsJson != null && !optionsJson.isEmpty()) {
                options = new JSONObject(optionsJson);
            }
            Log.i(LOG_TAG, "[DEBUG] requestPermissionWithString parsed options=" + options);

            UsbSerialManager manager = ensureSerialManager();
            int deviceId = extractDeviceId(options);
            if (deviceId < 0) {
                Log.w(LOG_TAG, "[WARN] requestPermissionWithString: deviceId is required, options=" + options);
                callback.invoke(errorResult("invalid_arguments", "deviceId is required"));
                return;
            }
            UsbSerialDeviceInfo info = findDeviceById(manager, deviceId);
            if (info == null) {
                Log.w(LOG_TAG, "[WARN] requestPermissionWithString: device not found with deviceId=" + deviceId);
                callback.invoke(errorResult("device_not_found", "No USB device with deviceId=" + deviceId));
                return;
            }
            if (manager.hasPermission(info.getUsbDevice())) {
                Log.i(LOG_TAG, "[DEBUG] requestPermissionWithString: permission already granted for device=" + deviceId);
                Map<String, Object> result = successResult();
                result.put("granted", true);
                callback.invoke(result);
                return;
            }
            ensurePermissionReceiver();
            pendingPermissionCallbacks.put(deviceId, callback);
            manager.requestPermission(info.getUsbDevice(), new UsbPermissionCallback() {
                @Override
                public void onPermissionGranted(android.hardware.usb.UsbDevice device) {
                    Log.i(LOG_TAG, "[DEBUG] Permission granted for device=" + device.getDeviceId());
                    handlePermissionResult(device, true);
                }

                @Override
                public void onPermissionDenied(android.hardware.usb.UsbDevice device) {
                    Log.w(LOG_TAG, "[WARN] Permission denied for device=" + device.getDeviceId());
                    handlePermissionResult(device, false);
                }
            });
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] requestPermissionWithString error", ex);
            callback.invoke(errorResult("request_permission_error", ex.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void requestPermission(JSONObject options, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] requestPermission invoked with options=" + options);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] requestPermission called without callback");
            return;
        }
        try {
            UsbSerialManager manager = ensureSerialManager();
            int deviceId = extractDeviceId(options);
            if (deviceId < 0) {
                Log.w(LOG_TAG, "[WARN] requestPermission: deviceId is required, options=" + options);
                callback.invoke(errorResult("invalid_arguments", "deviceId is required"));
                return;
            }
            UsbSerialDeviceInfo info = findDeviceById(manager, deviceId);
            if (info == null) {
                Log.w(LOG_TAG, "[WARN] requestPermission: device not found with deviceId=" + deviceId);
                callback.invoke(errorResult("device_not_found", "No USB device with deviceId=" + deviceId));
                return;
            }
            if (manager.hasPermission(info.getUsbDevice())) {
                Log.i(LOG_TAG, "[DEBUG] requestPermission: permission already granted for device=" + deviceId);
                Map<String, Object> result = successResult();
                result.put("granted", true);
                callback.invoke(result);
                return;
            }
            ensurePermissionReceiver();
            pendingPermissionCallbacks.put(deviceId, callback);
            manager.requestPermission(info.getUsbDevice(), new UsbPermissionCallback() {
                @Override
                public void onPermissionGranted(android.hardware.usb.UsbDevice device) {
                    Log.i(LOG_TAG, "[DEBUG] Permission granted for device=" + device.getDeviceId());
                    handlePermissionResult(device, true);
                }

                @Override
                public void onPermissionDenied(android.hardware.usb.UsbDevice device) {
                    Log.w(LOG_TAG, "[WARN] Permission denied for device=" + device.getDeviceId());
                    handlePermissionResult(device, false);
                }
            });
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] requestPermission error", ex);
            callback.invoke(errorResult("request_permission_error", ex.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void openWithString(String optionsJson, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] openWithString invoked with optionsJson=" + optionsJson);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] openWithString called without callback");
            return;
        }
        try {
            JSONObject options = null;
            if (optionsJson != null && !optionsJson.isEmpty()) {
                options = new JSONObject(optionsJson);
            }
            Log.i(LOG_TAG, "[DEBUG] openWithString parsed options=" + options);

            UsbSerialManager manager = ensureSerialManager();
            int deviceId = extractDeviceId(options);
            if (deviceId < 0) {
                Log.w(LOG_TAG, "[WARN] openWithString: deviceId is required");
                callback.invoke(errorResult("invalid_arguments", "deviceId is required"));
                return;
            }
            UsbSerialDeviceInfo info = findDeviceById(manager, deviceId);
            if (info == null) {
                Log.w(LOG_TAG, "[WARN] openWithString: device not found with deviceId=" + deviceId);
                callback.invoke(errorResult("device_not_found", "No USB device with deviceId=" + deviceId));
                return;
            }
            if (!manager.hasPermission(info.getUsbDevice())) {
                Log.w(LOG_TAG, "[WARN] openWithString: USB permission not granted for device=" + deviceId);
                callback.invoke(errorResult("permission_missing", "USB permission not granted"));
                return;
            }
            SerialConfig config = buildSerialConfig(options != null ? options.optJSONObject("config") : null, options);
            int interfaceIndex = options != null ? options.optInt("interfaceIndex", -1) : -1;
            SerialPort port = manager.openPort(info, config, interfaceIndex);
            SerialSession session = createSession(info, port, options);
            sessions.put(session.sessionId, session);
            Map<String, Object> result = successResult();
            result.put("sessionId", session.sessionId);
            result.put("deviceId", info.getUsbDevice().getDeviceId());
            result.put("config", serialConfigToMap(config));
            callback.invoke(result);
            Log.i(LOG_TAG, "[DEBUG] openWithString succeeded for session=" + session.sessionId);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] openWithString error", ex);
            callback.invoke(errorResult("open_error", ex.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void open(JSONObject options, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] open invoked with options=" + options);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] open called without callback");
            return;
        }
        try {
            UsbSerialManager manager = ensureSerialManager();
            int deviceId = extractDeviceId(options);
            if (deviceId < 0) {
                Log.w(LOG_TAG, "[WARN] open: deviceId is required, options=" + options);
                callback.invoke(errorResult("invalid_arguments", "deviceId is required"));
                return;
            }
            UsbSerialDeviceInfo info = findDeviceById(manager, deviceId);
            if (info == null) {
                Log.w(LOG_TAG, "[WARN] open: device not found with deviceId=" + deviceId);
                callback.invoke(errorResult("device_not_found", "No USB device with deviceId=" + deviceId));
                return;
            }
            if (!manager.hasPermission(info.getUsbDevice())) {
                Log.w(LOG_TAG, "[WARN] open: USB permission not granted for device=" + deviceId);
                callback.invoke(errorResult("permission_missing", "USB permission not granted"));
                return;
            }
            SerialConfig config = buildSerialConfig(options != null ? options.optJSONObject("config") : null, options);
            int interfaceIndex = options != null ? options.optInt("interfaceIndex", -1) : -1;
            SerialPort port = manager.openPort(info, config, interfaceIndex);
            SerialSession session = createSession(info, port, options);
            sessions.put(session.sessionId, session);
            Map<String, Object> result = successResult();
            result.put("sessionId", session.sessionId);
            result.put("deviceId", info.getUsbDevice().getDeviceId());
            result.put("config", serialConfigToMap(config));
            callback.invoke(result);
            Log.i(LOG_TAG, "[DEBUG] open succeeded for session=" + session.sessionId);
        } catch (PermissionDeniedException denied) {
            Log.w(LOG_TAG, "[WARN] open denied", denied);
            callback.invoke(errorResult("permission_denied", denied.getMessage()));
        } catch (UsbSerialException serialError) {
            Log.e(LOG_TAG, "[ERROR] open serial error", serialError);
            callback.invoke(errorResult("open_error", serialError.getMessage()));
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] open unexpected error", ex);
            callback.invoke(errorResult("open_error", ex.getMessage()));
        }
    }

    /**
     * Robustly extract deviceId from options, tolerating number/string/alias/nested forms.
     */
    private int extractDeviceId(JSONObject options) {
        Log.d(LOG_TAG, "[DEBUG] extractDeviceId called with options=" + options);
        if (options == null) {
            Log.w(LOG_TAG, "[WARN] extractDeviceId: options is null");
            return -1;
        }
        try {
            // Generic extraction: handle Number directly
            if (options.has("deviceId")) {
                Object raw = options.get("deviceId");
                Log.d(LOG_TAG, "[DEBUG] extractDeviceId: found deviceId key, raw value=" + raw + ", type=" + (raw != null ? raw.getClass().getName() : "null"));
                if (raw instanceof Number) {
                    int n = ((Number) raw).intValue();
                    Log.d(LOG_TAG, "[DEBUG] extractDeviceId: deviceId is Number, intValue=" + n);
                    if (n >= 0) return n;
                } else if (raw instanceof String) {
                    String s0 = ((String) raw).trim();
                    Log.d(LOG_TAG, "[DEBUG] extractDeviceId: deviceId is String, value=\"" + s0 + "\"");
                    try { return Integer.parseInt(s0); } catch (Exception ignored) {}
                } else if (raw instanceof JSONObject) {
                    JSONObject d0 = (JSONObject) raw;
                    Log.d(LOG_TAG, "[DEBUG] extractDeviceId: deviceId is JSONObject, value=" + d0);
                    int nid = d0.optInt("deviceId", -1);
                    if (nid >= 0) return nid;
                    String s1 = d0.optString("deviceId", null);
                    if (s1 != null) { try { return Integer.parseInt(s1.trim()); } catch (Exception ignored) {} }
                } else {
                    Log.w(LOG_TAG, "[WARN] extractDeviceId: deviceId has unexpected type: " + raw.getClass().getName());
                }
            } else {
                Log.w(LOG_TAG, "[WARN] extractDeviceId: options does not contain 'deviceId' key, keys=" + options.keys());
            }
            // Primary key as number
            int id = options.optInt("deviceId", -1);
            Log.d(LOG_TAG, "[DEBUG] extractDeviceId: optInt('deviceId', -1) returned " + id);
            if (id >= 0) return id;
            long lid = options.optLong("deviceId", -1L);
            if (lid >= 0L) return (int) lid;

            // Primary key as string
            String s = options.optString("deviceId", null);
            if (s != null) {
                try { return Integer.parseInt(s.trim()); } catch (Exception ignored) {}
            }

            // Alias keys
            id = options.optInt("id", -1);
            if (id >= 0) return id;
            s = options.optString("id", null);
            if (s != null) {
                try { return Integer.parseInt(s.trim()); } catch (Exception ignored) {}
            }
            id = options.optInt("deviceID", -1);
            if (id >= 0) return id;
            s = options.optString("deviceID", null);
            if (s != null) {
                try { return Integer.parseInt(s.trim()); } catch (Exception ignored) {}
            }

            // Nested device object
            JSONObject dev = options.optJSONObject("device");
            if (dev != null) {
                id = dev.optInt("deviceId", -1);
                if (id >= 0) return id;
                s = dev.optString("deviceId", null);
                if (s != null) {
                    try { return Integer.parseInt(s.trim()); } catch (Exception ignored) {}
                }
            }
        } catch (Exception e) {
            Log.w(LOG_TAG, "[WARN] extractDeviceId exception", e);
        }
        Log.w(LOG_TAG, "[WARN] extractDeviceId failed, options=" + options);
        return -1;
    }
    @UniJSMethod(uiThread = false)
    public void closeWithString(String optionsJson, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] closeWithString invoked with optionsJson=" + optionsJson);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] closeWithString called without callback");
            return;
        }
        try {
            JSONObject options = null;
            if (optionsJson != null && !optionsJson.isEmpty()) {
                options = new JSONObject(optionsJson);
            }
            Log.i(LOG_TAG, "[DEBUG] closeWithString parsed options=" + options);

            String sessionId = options != null ? options.optString("sessionId", null) : null;
            if (sessionId == null) {
                Log.w(LOG_TAG, "[WARN] closeWithString: sessionId is required");
                callback.invoke(errorResult("invalid_arguments", "sessionId is required"));
                return;
            }
            SerialSession session = sessions.remove(sessionId);
            if (session == null) {
                Log.w(LOG_TAG, "[WARN] closeWithString: session not found with id=" + sessionId);
                callback.invoke(errorResult("session_not_found", "No session with id=" + sessionId));
                return;
            }
            session.closeSilently();
            callback.invoke(successResult());
            Log.i(LOG_TAG, "[DEBUG] closeWithString: Session closed: " + sessionId);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] closeWithString error", ex);
            callback.invoke(errorResult("close_error", ex.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void close(JSONObject options, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] close invoked with options=" + options);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] close called without callback");
            return;
        }
        String sessionId = options != null ? options.optString("sessionId", null) : null;
        if (sessionId == null) {
            Log.w(LOG_TAG, "[WARN] close: sessionId is required");
            callback.invoke(errorResult("invalid_arguments", "sessionId is required"));
            return;
        }
        SerialSession session = sessions.remove(sessionId);
        if (session == null) {
            Log.w(LOG_TAG, "[WARN] close: session not found with id=" + sessionId);
            callback.invoke(errorResult("session_not_found", "No session with id=" + sessionId));
            return;
        }
        session.closeSilently();
        callback.invoke(successResult());
        Log.i(LOG_TAG, "[DEBUG] Session closed: " + sessionId);
    }

    @UniJSMethod(uiThread = false)
    public void writeWithString(String optionsJson, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] writeWithString invoked with optionsJson=" + optionsJson);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] writeWithString called without callback");
            return;
        }
        try {
            JSONObject options = null;
            if (optionsJson != null && !optionsJson.isEmpty()) {
                options = new JSONObject(optionsJson);
            }
            Log.i(LOG_TAG, "[DEBUG] writeWithString parsed options=" + options);

            String sessionId = options != null ? options.optString("sessionId", null) : null;
            if (sessionId == null) {
                Log.w(LOG_TAG, "[WARN] writeWithString: sessionId is required");
                callback.invoke(errorResult("invalid_arguments", "sessionId is required"));
                return;
            }
            SerialSession session = sessions.get(sessionId);
            if (session == null) {
                Log.w(LOG_TAG, "[WARN] writeWithString: session not found with id=" + sessionId);
                callback.invoke(errorResult("session_not_found", "No session with id=" + sessionId));
                return;
            }
            byte[] buffer = extractPayload(options);
            if (buffer == null || buffer.length == 0) {
                Log.w(LOG_TAG, "[WARN] writeWithString: no payload provided");
                callback.invoke(errorResult("invalid_arguments", "No payload provided"));
                return;
            }
            session.write(buffer);
            Log.i(LOG_TAG, "[DEBUG] writeWithString: " + buffer.length + " bytes written to session=" + sessionId);
            Map<String, Object> result = successResult();
            result.put("bytesWritten", buffer.length);
            callback.invoke(result);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] writeWithString failed", ex);
            callback.invoke(errorResult("write_error", ex.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void write(JSONObject options, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] write invoked with options=" + options);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] write called without callback");
            return;
        }
        String sessionId = options != null ? options.optString("sessionId", null) : null;
        if (sessionId == null) {
            Log.w(LOG_TAG, "[WARN] write: sessionId is required");
            callback.invoke(errorResult("invalid_arguments", "sessionId is required"));
            return;
        }
        SerialSession session = sessions.get(sessionId);
        if (session == null) {
            Log.w(LOG_TAG, "[WARN] write: session not found with id=" + sessionId);
            callback.invoke(errorResult("session_not_found", "No session with id=" + sessionId));
            return;
        }
        try {
            byte[] buffer = extractPayload(options);
            if (buffer == null || buffer.length == 0) {
                Log.w(LOG_TAG, "[WARN] write: no payload provided");
                callback.invoke(errorResult("invalid_arguments", "No payload provided"));
                return;
            }
            session.write(buffer);
            Log.i(LOG_TAG, "[DEBUG] write: " + buffer.length + " bytes written to session=" + sessionId);
            Map<String, Object> result = successResult();
            result.put("bytesWritten", buffer.length);
            callback.invoke(result);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] write failed", ex);
            callback.invoke(errorResult("write_error", ex.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void subscribeWithString(String optionsJson, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] subscribeWithString invoked with optionsJson=" + optionsJson);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] subscribeWithString called without callback");
            return;
        }
        try {
            JSONObject options = null;
            if (optionsJson != null && !optionsJson.isEmpty()) {
                options = new JSONObject(optionsJson);
            }
            Log.i(LOG_TAG, "[DEBUG] subscribeWithString parsed options=" + options);

            String sessionId = options != null ? options.optString("sessionId", null) : null;
            if (sessionId == null) {
                Log.w(LOG_TAG, "[WARN] subscribeWithString: sessionId is required");
                callback.invoke(errorResult("invalid_arguments", "sessionId is required"));
                return;
            }
            SerialSession session = sessions.get(sessionId);
            if (session == null) {
                Log.w(LOG_TAG, "[WARN] subscribeWithString: session not found with id=" + sessionId);
                callback.invoke(errorResult("session_not_found", "No session with id=" + sessionId));
                return;
            }
            String token = session.addSubscriber(callback);
            Log.i(LOG_TAG, "[DEBUG] subscribeWithString: added subscriber with token=" + token + " for session=" + sessionId);
            Map<String, Object> result = successResult();
            result.put("subscriptionId", token);
            result.put("type", "subscribed");
            callback.invokeAndKeepAlive(result);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] subscribeWithString error", ex);
            callback.invoke(errorResult("subscribe_error", ex.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void subscribe(JSONObject options, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] subscribe invoked with options=" + options);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] subscribe called without callback");
            return;
        }
        String sessionId = options != null ? options.optString("sessionId", null) : null;
        if (sessionId == null) {
            Log.w(LOG_TAG, "[WARN] subscribe: sessionId is required");
            callback.invoke(errorResult("invalid_arguments", "sessionId is required"));
            return;
        }
        SerialSession session = sessions.get(sessionId);
        if (session == null) {
            Log.w(LOG_TAG, "[WARN] subscribe: session not found with id=" + sessionId);
            callback.invoke(errorResult("session_not_found", "No session with id=" + sessionId));
            return;
        }
        String token = session.addSubscriber(callback);
        Log.i(LOG_TAG, "[DEBUG] subscribe: added subscriber with token=" + token + " for session=" + sessionId);
        Map<String, Object> result = successResult();
        result.put("subscriptionId", token);
        result.put("type", "subscribed");
        callback.invokeAndKeepAlive(result);
    }

    @UniJSMethod(uiThread = false)
    public void unsubscribeWithString(String optionsJson, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] unsubscribeWithString invoked with optionsJson=" + optionsJson);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] unsubscribeWithString called without callback");
            return;
        }
        try {
            JSONObject options = null;
            if (optionsJson != null && !optionsJson.isEmpty()) {
                options = new JSONObject(optionsJson);
            }
            Log.i(LOG_TAG, "[DEBUG] unsubscribeWithString parsed options=" + options);

            String sessionId = options != null ? options.optString("sessionId", null) : null;
            String subscriptionId = options != null ? options.optString("subscriptionId", null) : null;
            if (sessionId == null || subscriptionId == null) {
                Log.w(LOG_TAG, "[WARN] unsubscribeWithString: sessionId and subscriptionId are required");
                callback.invoke(errorResult("invalid_arguments", "sessionId and subscriptionId are required"));
                return;
            }
            SerialSession session = sessions.get(sessionId);
            if (session == null) {
                Log.w(LOG_TAG, "[WARN] unsubscribeWithString: session not found with id=" + sessionId);
                callback.invoke(errorResult("session_not_found", "No session with id=" + sessionId));
                return;
            }
            boolean removed = session.removeSubscriber(subscriptionId);
            if (!removed) {
                Log.w(LOG_TAG, "[WARN] unsubscribeWithString: subscription not found with id=" + subscriptionId);
                callback.invoke(errorResult("subscription_not_found", "No subscription with id=" + subscriptionId));
                return;
            }
            callback.invoke(successResult());
            Log.i(LOG_TAG, "[DEBUG] unsubscribeWithString: subscription removed for session=" + sessionId);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] unsubscribeWithString error", ex);
            callback.invoke(errorResult("unsubscribe_error", ex.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void unsubscribe(JSONObject options, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] unsubscribe invoked with options=" + options);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] unsubscribe called without callback");
            return;
        }
        String sessionId = options != null ? options.optString("sessionId", null) : null;
        String subscriptionId = options != null ? options.optString("subscriptionId", null) : null;
        if (sessionId == null || subscriptionId == null) {
            Log.w(LOG_TAG, "[WARN] unsubscribe: sessionId and subscriptionId are required");
            callback.invoke(errorResult("invalid_arguments", "sessionId and subscriptionId are required"));
            return;
        }
        SerialSession session = sessions.get(sessionId);
        if (session == null) {
            Log.w(LOG_TAG, "[WARN] unsubscribe: session not found with id=" + sessionId);
            callback.invoke(errorResult("session_not_found", "No session with id=" + sessionId));
            return;
        }
        boolean removed = session.removeSubscriber(subscriptionId);
        if (!removed) {
            Log.w(LOG_TAG, "[WARN] unsubscribe: subscription not found with id=" + subscriptionId);
            callback.invoke(errorResult("subscription_not_found", "No subscription with id=" + subscriptionId));
            return;
        }
        callback.invoke(successResult());
    }

    @UniJSMethod(uiThread = false)
    public void getStatusWithString(String optionsJson, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] getStatusWithString invoked with optionsJson=" + optionsJson);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] getStatusWithString called without callback");
            return;
        }
        try {
            JSONObject options = null;
            if (optionsJson != null && !optionsJson.isEmpty()) {
                options = new JSONObject(optionsJson);
            }
            Log.i(LOG_TAG, "[DEBUG] getStatusWithString parsed options=" + options);

            String sessionId = options != null ? options.optString("sessionId", null) : null;
            if (sessionId == null) {
                Log.w(LOG_TAG, "[WARN] getStatusWithString: sessionId is required");
                callback.invoke(errorResult("invalid_arguments", "sessionId is required"));
                return;
            }
            SerialSession session = sessions.get(sessionId);
            if (session == null) {
                Log.w(LOG_TAG, "[WARN] getStatusWithString: session not found with id=" + sessionId);
                callback.invoke(errorResult("session_not_found", "No session with id=" + sessionId));
                return;
            }
            Map<String, Object> status = new HashMap<>();
            status.put("sessionId", session.sessionId);
            status.put("deviceId", session.deviceId);
            status.put("open", session.isOpen());
            status.put("subscriberCount", session.subscriberCount());
            Map<String, Object> result = successResult();
            result.put("status", status);
            Log.i(LOG_TAG, "[DEBUG] getStatusWithString: returning status for session=" + sessionId);
            callback.invoke(result);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "[ERROR] getStatusWithString error", ex);
            callback.invoke(errorResult("get_status_error", ex.getMessage()));
        }
    }

    @UniJSMethod(uiThread = false)
    public void getStatus(JSONObject options, UniJSCallback callback) {
        Log.i(LOG_TAG, "[DEBUG] getStatus invoked with options=" + options);
        if (callback == null) {
            Log.w(LOG_TAG, "[WARN] getStatus called without callback");
            return;
        }
        String sessionId = options != null ? options.optString("sessionId", null) : null;
        if (sessionId == null) {
            Log.w(LOG_TAG, "[WARN] getStatus: sessionId is required");
            callback.invoke(errorResult("invalid_arguments", "sessionId is required"));
            return;
        }
        SerialSession session = sessions.get(sessionId);
        if (session == null) {
            Log.w(LOG_TAG, "[WARN] getStatus: session not found with id=" + sessionId);
            callback.invoke(errorResult("session_not_found", "No session with id=" + sessionId));
            return;
        }
        Map<String, Object> status = new HashMap<>();
        status.put("sessionId", session.sessionId);
        status.put("deviceId", session.deviceId);
        status.put("open", session.isOpen());
        status.put("subscriberCount", session.subscriberCount());
        Map<String, Object> result = successResult();
        result.put("status", status);
        Log.i(LOG_TAG, "[DEBUG] getStatus: returning status for session=" + sessionId);
        callback.invoke(result);
    }

    private SerialSession createSession(UsbSerialDeviceInfo info, SerialPort port, JSONObject options) throws UsbSerialException {
        SerialSession session = new SerialSession(info, port);
        boolean opened = session.open();
        if (!opened) {
            throw new UsbSerialException("Failed to open serial port");
        }
        if (options != null && options.has("dtr")) {
            session.setDtr(options.optBoolean("dtr"));
        }
        if (options != null && options.has("rts")) {
            session.setRts(options.optBoolean("rts"));
        }
        return session;
    }

    private UsbSerialDeviceInfo findDeviceById(UsbSerialManager manager, int deviceId) {
        List<UsbSerialDeviceInfo> devices = manager.listDevices();
        for (UsbSerialDeviceInfo info : devices) {
            if (info.getUsbDevice().getDeviceId() == deviceId) {
                return info;
            }
        }
        return null;
    }

    private SerialConfig buildSerialConfig(JSONObject configJson, JSONObject fallback) {
        SerialConfig.Builder builder = SerialConfig.builder();
        JSONObject source = configJson != null ? configJson : fallback;
        if (source == null) {
            return builder.build();
        }
        if (source.has("baudRate")) {
            builder.baudRate(source.optInt("baudRate"));
        }
        if (source.has("dataBits")) {
            builder.dataBits(source.optInt("dataBits"));
        }
        if (source.has("stopBits")) {
            builder.stopBits(source.optInt("stopBits"));
        }
        if (source.has("parity")) {
            builder.parity(source.optInt("parity"));
        }
        if (source.has("flowControl")) {
            builder.flowControl(source.optInt("flowControl"));
        }
        if (source.has("readTimeout")) {
            builder.readTimeoutMillis(source.optInt("readTimeout"));
        }
        if (source.has("writeTimeout")) {
            builder.writeTimeoutMillis(source.optInt("writeTimeout"));
        }
        if (source.has("readBufferSize")) {
            builder.readBufferSize(source.optInt("readBufferSize"));
        }
        return builder.build();
    }

    private byte[] extractPayload(JSONObject options) {
        if (options == null) {
            return null;
        }
        if (options.has("base64")) {
            String base64 = options.optString("base64", null);
            if (base64 != null) {
                return Base64.decode(base64, Base64.NO_WRAP);
            }
        }
        if (options.has("hex")) {
            String hex = options.optString("hex", null);
            if (hex != null) {
                return hexToBytes(hex);
            }
        }
        if (options.has("string")) {
            String value = options.optString("string", null);
            if (value != null) {
                String charsetName = options.optString("encoding", DEFAULT_CHARSET.name());
                Charset charset;
                try {
                    charset = Charset.forName(charsetName);
                } catch (Exception error) {
                    charset = DEFAULT_CHARSET;
                }
                return value.getBytes(charset);
            }
        }
        return null;
    }

    private Map<String, Object> deviceInfoToMap(UsbSerialDeviceInfo info, boolean hasPermission) {
        Map<String, Object> map = new HashMap<>();
        map.put("deviceId", info.getUsbDevice().getDeviceId());
        map.put("vendorId", info.getVendorId());
        map.put("productId", info.getProductId());
        map.put("deviceName", info.getDeviceName());
        map.put("manufacturerName", info.getManufacturerName());
        map.put("productName", info.getProductName());
        map.put("serialNumber", info.getSerialNumber());
        map.put("interfaceCount", info.getInterfaceCount());
        map.put("driverType", info.getDriverType().name());
        map.put("hasPermission", hasPermission);
        return map;
    }

    private Map<String, Object> serialConfigToMap(SerialConfig config) {
        Map<String, Object> map = new HashMap<>();
        map.put("baudRate", config.getBaudRate());
        map.put("dataBits", config.getDataBits());
        map.put("stopBits", config.getStopBits());
        map.put("parity", config.getParity());
        map.put("flowControl", config.getFlowControl());
        map.put("readBufferSize", config.getReadBufferSize());
        map.put("readTimeout", config.getReadTimeoutMillis());
        map.put("writeTimeout", config.getWriteTimeoutMillis());
        return map;
    }

    private Map<String, Object> successResult() {
        Map<String, Object> map = new HashMap<>();
        map.put("success", true);
        return map;
    }

    private Map<String, Object> errorResult(String code, String message) {
        Map<String, Object> map = new HashMap<>();
        map.put("success", false);
        map.put("code", code);
        map.put("message", message);
        return map;
    }

    private byte[] hexToBytes(String hex) {
        String clean = hex.replaceAll("\\s+", "");
        if (clean.length() % 2 != 0) {
            clean = "0" + clean;
        }
        int len = clean.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(clean.charAt(i), 16) << 4)
                    + Character.digit(clean.charAt(i + 1), 16));
        }
        return data;
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(String.format("%02X", b));
        }
        return sb.toString();
    }

    private String safeString(byte[] bytes) {
        try {
            return new String(bytes, DEFAULT_CHARSET);
        } catch (Exception ignored) {
            return null;
        }
    }

    private final class SerialSession {
        private final String sessionId = UUID.randomUUID().toString();
        private final UsbSerialDeviceInfo deviceInfo;
        private final int deviceId;
        private final SerialPort port;
        private final ConcurrentMap<String, UniJSCallback> subscribers = new ConcurrentHashMap<>();
        private final AtomicBoolean closing = new AtomicBoolean(false);
        private final DataListener dataListener;

        SerialSession(UsbSerialDeviceInfo info, SerialPort port) {
            this.deviceInfo = info;
            this.deviceId = info.getUsbDevice().getDeviceId();
            this.port = port;
            this.dataListener = new DataListener() {
                @Override
                public void onDataReceived(byte[] data) {
                    dispatchData(data);
                }

                @Override
                public void onError(Exception error) {
                    dispatchError(error);
                }
            };
            this.port.addDataListener(this.dataListener);
        }

        boolean open() throws UsbSerialException {
            return port.open();
        }

        void write(byte[] payload) throws UsbSerialException {
            port.write(payload);
        }

        void setDtr(boolean value) throws UsbSerialException {
            port.setDTR(value);
        }

        void setRts(boolean value) throws UsbSerialException {
            port.setRTS(value);
        }

        boolean isOpen() {
            return port.isOpen();
        }

        int subscriberCount() {
            return subscribers.size();
        }

        String addSubscriber(UniJSCallback callback) {
            String token = UUID.randomUUID().toString();
            subscribers.put(token, callback);
            return token;
        }

        boolean removeSubscriber(String token) {
            return subscribers.remove(token) != null;
        }

        void dispatchData(byte[] data) {
            if (subscribers.isEmpty()) {
                return;
            }
            Map<String, Object> payload = new HashMap<>();
            payload.put("sessionId", sessionId);
            payload.put("deviceId", deviceId);
            payload.put("timestamp", System.currentTimeMillis());
            payload.put("length", data.length);
            payload.put("base64", Base64.encodeToString(data, Base64.NO_WRAP));
            payload.put("hex", bytesToHex(data));
            payload.put("type", "data");
            String text = safeString(data);
            if (text != null && !text.isEmpty()) {
                payload.put("text", text);
            }
            for (UniJSCallback callback : subscribers.values()) {
                try {
                    callback.invokeAndKeepAlive(payload);
                } catch (Exception ex) {
                    Log.e(LOG_TAG, "[ERROR] Failed to dispatch data to subscriber", ex);
                }
            }
        }

        void dispatchError(Exception error) {
            if (subscribers.isEmpty()) {
                return;
            }
            Map<String, Object> payload = new HashMap<>();
            payload.put("sessionId", sessionId);
            payload.put("deviceId", deviceId);
            payload.put("timestamp", System.currentTimeMillis());
            payload.put("error", error != null ? error.getMessage() : "unknown error");
            payload.put("type", "error");
            for (UniJSCallback callback : subscribers.values()) {
                try {
                    callback.invokeAndKeepAlive(payload);
                } catch (Exception ex) {
                    Log.e(LOG_TAG, "[ERROR] Failed to dispatch error to subscriber", ex);
                }
            }
        }

        void closeSilently() {
            if (!closing.compareAndSet(false, true)) {
                return;
            }
            subscribers.clear();
            port.removeDataListener(dataListener);
            try {
                port.close();
            } catch (IOException ex) {
                Log.w(LOG_TAG, "[WARN] Error closing serial port", ex);
            }
        }
    }
}